package com.hww.common.util;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.storage.StorageManager;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;

import androidx.core.content.FileProvider;

import com.hww.common.MyAppThis;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Method;

public final class UriUtils {

    private UriUtils() {
        throw new UnsupportedOperationException("u can't instantiate me...");
    }

    /**
     * 将资源文件路径转换为Uri格式。
     * 该方法主要用于将应用中的资源文件路径转换为Uri对象，以便在需要Uri的地方使用，如加载图片到ImageView。
     * <p>
     * 支持两种形式的资源路径输入：
     * 1. 以资源类型和名称组成的路径，如"drawable/icon"或"raw/icon"。
     * 2. 直接使用资源ID，如"R.drawable.icon"。
     * </p>
     * 注意：调用者需要确保提供的资源路径有效，否则将返回一个无效的Uri。
     *
     * @param resPath 资源文件的路径或资源ID的字符串表示。
     * @return 返回转换后的Uri对象。
     */
    public static Uri res2Uri(String resPath) {
        return Uri.parse("android.resource://" + MyAppThis.getThis().getPackageName() + "/" + resPath);
    }

    /**
     * 将文件转换为Uri格式。
     * <p>
     * 本方法用于根据不同的Android版本，选择合适的方式将文件转换为Uri对象。
     * 在Android N及以后的版本中，由于引入了更严格的文件访问策略，需要使用FileProvider来获取Uri。
     * 在之前的版本中，可以直接从文件路径创建Uri。
     *
     * @param file 需要转换的文件对象。
     * @return 转换后的Uri对象，如果文件不存在或不支持该功能，则返回null。
     */
    public static Uri file2Uri(final File file) {
        if (!FileUtils.isFileExists(file)) return null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            /*构建FileProvider的authority，它由应用包名和.file_provider组成，在AndroidManifest中provider的item中配置*/
            String authority = MyAppThis.getThis().getPackageName() + ".file_provider";
            return FileProvider.getUriForFile(MyAppThis.getThis(), authority, file);
        } else {
            return Uri.fromFile(file);
        }
    }

    /**
     * 将Uri转换为文件对象，同时转换成功后对缓存进行复制。
     *
     * @param uri 需要转换的Uri对象.
     * @return 转换后的文件对象，如果Uri为空或转换失败，则返回null。
     */
    public static File uri2File(final Uri uri) {
        if (uri == null) return null;
        File file = uri2FileReal(uri);
        if (file != null) return file;
        return copyUri2Cache(uri);
    }

    /**
     * 将Uri转换为文件对象，不进行缓存复制。
     * <p>
     * 此方法旨在通过Uri获取对应的文件对象，特别适用于需要直接操作文件的场景。
     * 它避免了缓存复制的过程，从而节省了存储空间和时间。
     * </p>
     *
     * @param uri 需要转换的Uri对象，代表要操作的文件。
     * @return 返回转换后的文件对象；如果Uri为空，则返回null。
     */
    public static File uri2FileNoCacheCopy(final Uri uri) {
        if (uri == null) return null;
        return uri2FileReal(uri);
    }

    /**
     * 将Uri转换为文件对象。
     * 主要处理来自不同来源的Uri，包括内部存储、外部存储、下载文档和媒体文档等。
     * 对于每个Uri类型，都有特定的处理逻辑来构造文件路径。
     *
     * @param uri 需要转换的Uri对象。
     * @return 返回对应的文件对象，如果无法转换则返回null。
     */
    private static File uri2FileReal(final Uri uri) {
        Log.d("UriUtils", uri.toString());
        String authority = uri.getAuthority();
        String scheme = uri.getScheme();
        String path = uri.getPath();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && path != null) {
            String[] externals = new String[]{"/external/", "/external_path/"};
            File file = null;
            for (String external : externals) {
                if (path.startsWith(external)) {
                    file = new File(Environment.getExternalStorageDirectory().getAbsolutePath()
                            + path.replace(external, "/"));
                    if (file.exists()) {
                        Log.d("UriUtils", uri.toString() + " -> " + external);
                        return file;
                    }
                }
            }
            file = null;
            if (path.startsWith("/files_path/")) {
                file = new File(MyAppThis.getThis().getFilesDir().getAbsolutePath()
                        + path.replace("/files_path/", "/"));
            } else if (path.startsWith("/cache_path/")) {
                file = new File(MyAppThis.getThis().getCacheDir().getAbsolutePath()
                        + path.replace("/cache_path/", "/"));
            } else if (path.startsWith("/external_files_path/")) {
                file = new File(MyAppThis.getThis().getExternalFilesDir(null).getAbsolutePath()
                        + path.replace("/external_files_path/", "/"));
            } else if (path.startsWith("/external_cache_path/")) {
                file = new File(MyAppThis.getThis().getExternalCacheDir().getAbsolutePath()
                        + path.replace("/external_cache_path/", "/"));
            }
            if (file != null && file.exists()) {
                Log.d("UriUtils", uri.toString() + " -> " + path);
                return file;
            }
        }
        if (ContentResolver.SCHEME_FILE.equals(scheme)) {
            if (path != null) return new File(path);
            Log.d("UriUtils", uri.toString() + " parse failed. -> 0");
            return null;
        }// end 0
        else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
                && DocumentsContract.isDocumentUri(MyAppThis.getThis(), uri)) {
            if ("com.android.externalstorage.documents".equals(authority)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];
                if ("primary".equalsIgnoreCase(type)) {
                    return new File(Environment.getExternalStorageDirectory() + "/" + split[1]);
                } else {
                    // Below logic is how External Storage provider build URI for documents
                    // http://stackoverflow.com/questions/28605278/android-5-sd-card-label
                    StorageManager mStorageManager = (StorageManager) MyAppThis.getThis().getSystemService(Context.STORAGE_SERVICE);
                    try {
                        Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
                        Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
                        Method getUuid = storageVolumeClazz.getMethod("getUuid");
                        Method getState = storageVolumeClazz.getMethod("getState");
                        Method getPath = storageVolumeClazz.getMethod("getPath");
                        Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
                        Method isEmulated = storageVolumeClazz.getMethod("isEmulated");

                        Object result = getVolumeList.invoke(mStorageManager);

                        final int length = Array.getLength(result);
                        for (int i = 0; i < length; i++) {
                            Object storageVolumeElement = Array.get(result, i);
                            //String uuid = (String) getUuid.invoke(storageVolumeElement);

                            final boolean mounted = Environment.MEDIA_MOUNTED.equals(getState.invoke(storageVolumeElement))
                                    || Environment.MEDIA_MOUNTED_READ_ONLY.equals(getState.invoke(storageVolumeElement));

                            //if the media is not mounted, we need not get the volume details
                            if (!mounted) continue;

                            //Primary storage is already handled.
                            if ((Boolean) isPrimary.invoke(storageVolumeElement)
                                    && (Boolean) isEmulated.invoke(storageVolumeElement)) {
                                continue;
                            }

                            String uuid = (String) getUuid.invoke(storageVolumeElement);

                            if (uuid != null && uuid.equals(type)) {
                                return new File(getPath.invoke(storageVolumeElement) + "/" + split[1]);
                            }
                        }
                    } catch (Exception ex) {
                        Log.d("UriUtils", uri.toString() + " parse failed. " + ex.toString() + " -> 1_0");
                    }
                }
                Log.d("UriUtils", uri.toString() + " parse failed. -> 1_0");
                return null;
            }// end 1_0
            else if ("com.android.providers.downloads.documents".equals(authority)) {
                String id = DocumentsContract.getDocumentId(uri);
                if (TextUtils.isEmpty(id)) {
                    Log.d("UriUtils", uri.toString() + " parse failed(id is null). -> 1_1");
                    return null;
                }
                if (id.startsWith("raw:")) {
                    return new File(id.substring(4));
                } else if (id.startsWith("msf:")) {
                    id = id.split(":")[1];
                }

                long availableId = 0;
                try {
                    availableId = Long.parseLong(id);
                } catch (Exception e) {
                    return null;
                }

                String[] contentUriPrefixesToTry = new String[]{
                        "content://downloads/public_downloads",
                        "content://downloads/all_downloads",
                        "content://downloads/my_downloads"
                };

                for (String contentUriPrefix : contentUriPrefixesToTry) {
                    Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), availableId);
                    try {
                        File file = getFileFromUri(contentUri, "1_1");
                        if (file != null) {
                            return file;
                        }
                    } catch (Exception ignore) {
                    }
                }

                Log.d("UriUtils", uri.toString() + " parse failed. -> 1_1");
                return null;
            }// end 1_1
            else if ("com.android.providers.media.documents".equals(authority)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];
                Uri contentUri;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                } else {
                    Log.d("UriUtils", uri.toString() + " parse failed. -> 1_2");
                    return null;
                }
                final String selection = "_id=?";
                final String[] selectionArgs = new String[]{split[1]};
                return getFileFromUri(contentUri, selection, selectionArgs, "1_2");
            }// end 1_2
            else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
                return getFileFromUri(uri, "1_3");
            }// end 1_3
            else {
                Log.d("UriUtils", uri.toString() + " parse failed. -> 1_4");
                return null;
            }// end 1_4
        }// end 1
        else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
            return getFileFromUri(uri, "2");
        }// end 2
        else {
            Log.d("UriUtils", uri.toString() + " parse failed. -> 3");
            return null;
        }// end 3
    }

    /**
     * 根据Uri获取文件对象。
     *
     * @param uri  文件的Uri，用于定位文件。
     * @param code 代码标识，用于日志输出，区分不同的调用场景。
     * @return 对应于Uri的文件对象，如果无法获取则返回null。
     */
    private static File getFileFromUri(final Uri uri, final String code) {
        return getFileFromUri(uri, null, null, code);
    }

    /**
     * 根据Uri获取文件对象。
     * 该方法主要用于处理来自不同应用提供的Uri，以获取对应的文件对象。
     * 对于不同的Uri authority，采取不同的处理策略来构造File对象。
     * 对于无法直接处理的Uri，则通过查询其对应的文件路径来构造File对象。
     *
     * @param uri           文件的Uri，用于定位文件。
     * @param selection     查询条件，用于过滤结果。
     * @param selectionArgs 查询条件的参数，用于填充查询条件。
     * @param code          代码标识，用于日志输出，区分不同的调用场景。
     * @return 对应于Uri的文件对象，如果无法获取则返回null。
     */
    private static File getFileFromUri(final Uri uri,
                                       final String selection,
                                       final String[] selectionArgs,
                                       final String code) {
        if ("com.google.android.apps.photos.content".equals(uri.getAuthority())) {
            if (!TextUtils.isEmpty(uri.getLastPathSegment())) {
                return new File(uri.getLastPathSegment());
            }
        } else if ("com.tencent.mtt.fileprovider".equals(uri.getAuthority())) {
            String path = uri.getPath();
            if (!TextUtils.isEmpty(path)) {
                File fileDir = Environment.getExternalStorageDirectory();
                return new File(fileDir, path.substring("/QQBrowser".length(), path.length()));
            }
        } else if ("com.huawei.hidisk.fileprovider".equals(uri.getAuthority())) {
            String path = uri.getPath();
            if (!TextUtils.isEmpty(path)) {
                return new File(path.replace("/root", ""));
            }
        }

        final Cursor cursor = MyAppThis.getThis().getContentResolver().query(
                uri, new String[]{"_data"}, selection, selectionArgs, null);
        if (cursor == null) {
            Log.d("UriUtils", uri.toString() + " parse failed(cursor is null). -> " + code);
            return null;
        }
        try {
            if (cursor.moveToFirst()) {
                final int columnIndex = cursor.getColumnIndex("_data");
                if (columnIndex > -1) {
                    return new File(cursor.getString(columnIndex));
                } else {
                    Log.d("UriUtils", uri.toString() + " parse failed(columnIndex: " + columnIndex + " is wrong). -> " + code);
                    return null;
                }
            } else {
                Log.d("UriUtils", uri.toString() + " parse failed(moveToFirst return false). -> " + code);
                return null;
            }
        } catch (Exception e) {
            Log.d("UriUtils", uri.toString() + " parse failed. -> " + code);
            return null;
        } finally {
            cursor.close();
        }
    }

    /**
     * 将URI指定的资源复制到应用的缓存目录。
     * <p>
     * 此方法用于处理URI，将其对应的资源复制到应用的缓存目录中(文件名使用系统当前毫秒值)，以便离线使用或进一步处理。
     * 主要用于处理来自外部存储器的文件，如相册图片等。
     *
     * @param uri 要复制的资源的URI。
     * @return 返回复制后的文件对象，如果复制失败则返回null。
     */
    private static File copyUri2Cache(Uri uri) {
        Log.d("UriUtils", "copyUri2Cache() called");
        InputStream is = null;
        try {
            is = MyAppThis.getThis().getContentResolver().openInputStream(uri);
            File file = new File(MyAppThis.getThis().getCacheDir(), "" + System.currentTimeMillis());
            FileIOUtils.writeFileFromIS(file.getAbsolutePath(), is);
            return file;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return null;
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 将Uri转换为字节数组。
     * <p>
     * 此方法通过输入流从给定的Uri加载数据，并将其转换为字节数组。
     * 主要用于处理存储在Uri中的媒体文件或其他二进制数据。
     *
     * @param uri 需要转换的Uri。如果Uri为空，方法将直接返回null。
     * @return 字节数组，表示从Uri加载的数据。如果Uri为空或发生错误，则返回null。
     */
    public static byte[] uri2Bytes(Uri uri) {
        if (uri == null) return null;
        InputStream is = null;
        try {
            is = MyAppThis.getThis().getContentResolver().openInputStream(uri);
            return ConvertUtils.inputStream2Bytes(is);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            Log.d("UriUtils", "uri to bytes failed.");
            return null;
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}